import { Component, ReactNode } from 'react'
import ScrollReveal from 'scrollreveal'

/**
 * `ScrollReveal()`采用单例模式；无论构造函数被调用多少次，它都将
 * 始终返回相同的实例。这意味着我们可以在任何地方调用它，无后顾之忧。
 */
const GmScrollreveal = (srOptions = {}) => (ComponentParam: ReactNode) => {

  const sr = ScrollReveal();
  return class extends Component {
    static displayName = 'ComponentWithScrollReveal';
    constructor(props: any) {
      super(props)
    }
    private animationContainer: any = undefined;
    getRevealElements(selector: string) {
      return selector ? this.animationContainer?.querySelectorAll(selector) :
        this.animationContainer;
    }
    getRef = (node: any) => {
      if (typeof node.nodeType === 'number') {
        this.animationContainer = node;
      } else {
        throw new Error('You should put animationContainerReference on DOM node, not React component.');
      }
    };
    applyRevealAnimation = ({ selector, options = {}, interval }: any) => {
      const revealElements = this.getRevealElements(selector);
      const opts = Object.assign({}, options);

      // revealElements can be NodeList or single node
      if (revealElements.length || !!revealElements.nodeType) {
        process.browser&&sr.reveal(revealElements, opts, interval);
      }
    };
    private initialize() {
      if (!this.animationContainer) {
        return;
      }
      this.forEachSrOption(this.applyRevealAnimation);
    }

    clean(cleanStyles: any) {
      // cleaning styles makes sr animation initialize again
      // on same element that were still in DOM
      if (cleanStyles) {
        this.forEachSrElement(process.browser&&sr.clean);
      } else {
        // remove event listeners
        // on component unmount event
        process.browser&&sr.destroy();
      }
    }

    refresh() {
      this.clean(true);
      this.initialize();
    }
    componentDidMount() {
      this.initialize();
    }
    componentWillUpdate() {
      this.refresh();
    }
    componentWillUnmount() {
      this.clean(false);
    }
    forEachSrElement = (fn: any) => {
      const elements: Array<any> = [];
      this.forEachSrOption(({ selector }: any) => {
        elements.concat(Array.prototype.slice.apply(process.browser && document.querySelectorAll(selector)));
      });
      elements.forEach(fn);
    };
    forEachSrOption = (fn: any) => {
      if (Array.isArray(srOptions)) {
        srOptions.forEach((options) => {
          fn(options);
        });
      } else if (typeof srOptions === 'object') {
        fn(srOptions);
      } else {
        throw new TypeError('Invalid arguments were passed');
      }
    };
    render() {
      return (
        <Component
          animationContainerReference={this.getRef}
          destroyRevealAnimation={this.clean}
          refreshRevealAnimation={this.refresh}
          {...this.props}
        />
      );
    }
  };
}

export default GmScrollreveal;
